/// Types of values we can deal with in an interpreter.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ValueType {
    /// Any "empty" type, regardless of if it's constructible or not.
    Void,

    /// byte
    U8,

    /// signed byte
    I8,

    /// short
    U16,

    /// signed short
    I16,

    /// int
    U32,

    /// signed int
    I32,

    /// unsigned large number, also used for lengths
    U64,

    /// signed large number
    I64,

    /// Unsigned size, whatever the native type for indexing is.
    USize,

    /// Signed difference between sizes.
    ISize,

    /// true/false, used in comparisons
    Bool,

    /// Reference to a range of bytes, indexable, possibly part of the
    /// underlying buffer or not, varying size.
    ByteSlice,

    /// UTF-8 unicode string as some kind of ref.  Same layout as a slice but
    /// might have some special conversion between this and a target language
    /// string type.
    StrUtf8Slice,

    /// Like slices, but for fixed sizes.
    ByteArray(usize),

    /// Owned instance of a user type (by ID), owning the underlying buffer.
    ObjectInst(UserTypeId),

    /// Reference to a user type (by ID), using a borrowed underlying buffer.
    ObjectRef(UserTypeId),

    /// Possibly missing instance of some type.
    Option(Box<ValueType>),

    /// Sequence of some type, possibly empty.
    List(Box<ValueType>),
}

impl ValueType {
    /// Creates a type that's an optional instance of some other type.
    pub fn option_of(ty: ValueType) -> Self {
        Self::Option(Box::new(ty))
    }

    /// Creates a type that's a list of instances of some other type.
    pub fn list_of(ty: ValueType) -> Self {
        Self::List(Box::new(ty))
    }

    /// Returns if this is an integer type.
    pub fn is_integer(&self) -> bool {
        use ValueType::*;
        match self {
            U8 | I8 | U16 | I16 | U32 | I32 | U64 | I64 | USize | ISize => true,
            _ => false,
        }
    }
}

impl From<IntType> for ValueType {
    fn from(value: IntType) -> Self {
        match value {
            IntType::U8 => Self::U8,
            IntType::I8 => Self::I8,
            IntType::U16 => Self::U16,
            IntType::I16 => Self::I16,
            IntType::U32 => Self::U32,
            IntType::I32 => Self::I32,
            IntType::U64 => Self::U64,
            IntType::I64 => Self::I64,
        }
    }
}

/// Integer types, which can be used to read to / write from the underlying
/// buffer in an endianness-aware way.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum IntType {
    U8,
    I8,
    U16,
    I16,
    U32,
    I32,
    U64,
    I64,
}

impl IntType {
    pub fn byte_len(&self) -> usize {
        use IntType::*;
        match self {
            U8 | I8 => 1,
            U16 | I16 => 2,
            U32 | I32 => 4,
            U64 | I64 => 8,
        }
    }

    pub fn is_signed(&self) -> bool {
        use IntType::*;
        match self {
            U8 | U16 | U32 | U64 => false,
            I8 | I16 | I32 | I64 => true,
        }
    }

    pub fn is_byte(&self) -> bool {
        match self {
            Self::U8 | Self::I8 => true,
            _ => false,
        }
    }

    /// Converts the int type to a big-endian encoding.
    pub fn to_big_encoding(&self) -> IntEncoding {
        IntEncoding::new(*self, Endianness::Big)
    }

    /// Converts the int type to a little-endian encoding.
    pub fn to_little_encoding(&self) -> IntEncoding {
        IntEncoding::new(*self, Endianness::Little)
    }
}

impl TryFrom<&ValueType> for IntType {
    type Error = ();

    fn try_from(value: &ValueType) -> Result<Self, Self::Error> {
        Ok(match value {
            ValueType::U8 => IntType::U8,
            ValueType::I8 => IntType::I8,
            ValueType::U16 => IntType::U16,
            ValueType::I16 => IntType::I16,
            ValueType::U32 => IntType::U32,
            ValueType::I32 => IntType::I32,
            ValueType::U64 => IntType::U64,
            ValueType::I64 => IntType::I64,
            _ => return Err(()),
        })
    }
}

/// Which direction the bytes are laid out.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Endianness {
    Big,
    Little,
}

impl Endianness {
    pub fn lower_str(&self) -> &'static str {
        match self {
            Self::Big => "big",
            Self::Little => "little",
        }
    }
}

/// Describes an integer encoding, ie the type (width and signedness) and the
/// endianness.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct IntEncoding {
    ty: IntType,
    endianness: Endianness,
}

impl IntEncoding {
    pub fn new(ty: IntType, endianness: Endianness) -> Self {
        Self { ty, endianness }
    }

    /// Encoding for a single byte.  Endianness is indeterminate for this, but
    /// we treat it as "big".
    pub fn byte() -> Self {
        Self::new(IntType::U8, Endianness::Big)
    }

    pub fn ty(&self) -> IntType {
        self.ty
    }

    pub fn endianness(&self) -> Endianness {
        self.endianness
    }
}

impl From<IntEncoding> for IntType {
    fn from(enc: IntEncoding) -> Self {
        enc.ty
    }
}

/// Identifies a user-level type, but not how it's referenced.  It might be
/// owned or unowned, depending on the context.
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct UserTypeId(u32);

impl UserTypeId {
    pub fn from_raw(v: u32) -> Self {
        Self(v)
    }
}
